﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
#if !PHONE
using System.Windows.Browser;
#endif
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Text.RegularExpressions;
#if !PHONE
using Microsoft.JScript.Runtime.Types;
#endif
using Microsoft.Popfly.GameCreator.Data;
using Microsoft.Popfly.GameCreator.GameEngine.Behaviors;
#if !PHONE
using Microsoft.Popfly.GameCreator.GameEngine.Behaviors.JSRuntime;
#endif

namespace Microsoft.Popfly.GameCreator.GameEngine
{
    public class SceneChangeEventArgs : EventArgs
    {
        public Scene newScene;
    }
    public class PausedOrUnPausedArgs : EventArgs
    {
        public bool isPaused;
    }
    public class Game : PropertyHolder, IDisposable
    {
        public static int EventListenerCount;

        public event EventHandler<GamePerformanceEventArgs> GamePerformanceDataUpdated;

        public event EventHandler<SceneChangeEventArgs> SceneChange;
        public event EventHandler<PausedOrUnPausedArgs> PausedOrUnPaused;

        public bool ShowDebugCollisionEdges { get; set; }

        public double Score { get; set; }

        public double DeltaTimeInSeconds { get; private set; }

        private Dictionary<Key, KeyEventArgs> m_keysDown = new Dictionary<Key, KeyEventArgs>();
        private Dictionary<Key, KeyEventArgs> m_keysDownThisFrame = new Dictionary<Key, KeyEventArgs>();
        private Dictionary<Key, KeyEventArgs> m_keysUp = new Dictionary<Key, KeyEventArgs>();
        private Dictionary<Key, KeyEventArgs> m_keysUpThisFrame = new Dictionary<Key, KeyEventArgs>();

        private bool m_gameIsRunning = false;

        public bool IsPaused
        {
            get
            {
                return !m_gameIsRunning;
            }
        }

        public bool IsHowToPlayShowing
        {
            get;
            private set;
        }
        private Scene m_currentScene;
        public Scene CurrentScene
        {
            get
            {
                return m_currentScene;
            }
            private set
            {
                if (m_currentScene != null)
                {
                    if (m_currentScene.MusicUrl != null && m_currentScene.MusicUrl != "")
                        StopAudio(m_currentScene.MusicUrl);
                    PlayArea.Children.Remove(m_currentScene.GetCanvasRoot());
                }

                EventListenerCount = 0;

                m_currentScene = value;
                m_currentScene.InitAllPropertyChangePreviousValues(); // now that CurrentScene is set, make sure to init all the property change values.

                if (value.MusicUrl != null & value.MusicUrl != "")
                    PlayAudio(value.MusicUrl, true);

                PlayArea.Children.Add(value.GetCanvasRoot());

                FitSceneToCanvas(value);

                m_pendingNewScene = null;
                m_keysDownThisFrame.Clear();
                m_keysUpThisFrame.Clear();
                CurrentScene.ClearAllMouseArgsForNextUpdate();

                value.Update(1.0 / 60.0);

                if (SceneChange != null)
                    SceneChange.Invoke(this, new SceneChangeEventArgs() { newScene = m_currentScene });
            }
        }

        public void FitSceneToCanvas()
        {
            FitSceneToCanvas(CurrentScene);
        }
        public void FitSceneToCanvas(Scene scene)
        {
            RectangleGeometry clipRect = new RectangleGeometry();
            PlayArea.Width = scene.ViewportWidth;
            PlayArea.Height = scene.ViewportHeight;
            clipRect.Rect = new Rect(0, 0, scene.ViewportWidth, scene.ViewportHeight);
            PlayArea.Clip = clipRect;

            var gameContainer = this.m_resizeToFitRoot.Parent as Canvas;
            Debug.Assert(gameContainer != null);
            var scale = System.Math.Min(gameContainer.Width / scene.ViewportWidth, gameContainer.Height / scene.ViewportHeight);


            ScaleTransform scaleTransform = new ScaleTransform();

            scaleTransform.ScaleX = scale;
            scaleTransform.ScaleY = scale;
            m_resizeToFitRoot.RenderTransform = scaleTransform;

            m_resizeToFitRoot.Width = scene.ViewportWidth;
            m_resizeToFitRoot.Height = scene.ViewportHeight;
            PlayArea.Width = scene.ViewportWidth;
            PlayArea.Height = scene.ViewportHeight;

            Canvas.SetLeft(m_resizeToFitRoot, (gameContainer.Width / 2 - (scene.ViewportWidth * scale) / 2));
            Canvas.SetTop(m_resizeToFitRoot, (gameContainer.Height / 2 - (scene.ViewportHeight * scale) / 2));
        }

        private DateTime m_lastUpdateTime;

        public TimeSpan ElapsedTime { get; private set; }
        public Canvas PlayArea { get; private set; }

        public Data.Resources GameResources { get; private set; }

        public Data.GameInfo m_gameInfo { get; private set; }

        private string m_pendingNewScene;
        private long m_gamePlayStart;
        private long m_completedGameDuration; // persist for signin

        public Random RandomNumberGenerator { get; private set; }

        public bool TestMode { get; private set; }

        private Canvas m_overlayCanvas;
        private Canvas m_resizeToFitRoot;
        private UserControl m_eventControl;
        private RuntimeBar m_runtimeBar;


        public Game(Data.GameInfo gameInfo, Data.Resources resources, UserControl eventControl, Canvas playArea, Canvas overlayCanvas, RuntimeBar runtimeBar, Canvas ResizeToFitRoot, bool testMode)
        {
            PlayArea = playArea;
            m_overlayCanvas = overlayCanvas;
            m_runtimeBar = runtimeBar;
            m_resizeToFitRoot = ResizeToFitRoot;
            m_gameInfo = gameInfo;
            GameResources = resources;
            m_eventControl = eventControl;
            TestMode = testMode;

            foreach (var pair in gameInfo.properties)
                SetValue(pair.Key, pair.Value.Value);

            eventControl.LostFocus += OnLostFocus;
            playArea.MouseMove += OnMouseMove;
            playArea.MouseLeftButtonUp += OnMouseUp;
            eventControl.KeyUp += OnKeyUp;
            eventControl.KeyDown += OnKeyDown;



            m_lastUpdateTime = DateTime.Now;
            m_timeOfLastReport = DateTime.Now;
            if (TestMode)
            {
                CompositionTarget.Rendering += TestLoopRenderingHandler;
                RandomNumberGenerator = new Test.NonRandom();
            }
            else
            {
                CompositionTarget.Rendering += GameLoopRenderingHandler;
                RandomNumberGenerator = new Random();
            }

            CurrentScene = new Scene(this, m_gameInfo.Actors, m_gameInfo.Scenes.getIndexedValue(0));

            m_gameIsRunning = true;
        }

        private void TestLoopRenderingHandler(object sender, EventArgs e)
        {
            // run at just faster than 30 fps
            DeltaTimeInSeconds = 0.03;
            Update(DeltaTimeInSeconds);
        }

        private int m_framesThisReport = 0;
        private DateTime m_timeOfLastReport;
        private TimeSpan m_timeSinceLastReport;
        private void GameLoopRenderingHandler(object sender, EventArgs e)
        {
            DateTime now = DateTime.Now;

            // update framerate counter
            m_framesThisReport++;
            m_timeSinceLastReport = now - m_timeOfLastReport;
            if (m_timeSinceLastReport.TotalSeconds > 0.5)
            {
                m_timeOfLastReport = now;

                double framesPerSecond = m_framesThisReport / m_timeSinceLastReport.TotalSeconds;
                if (GamePerformanceDataUpdated != null)
                {
                    GamePerformanceDataUpdated(this, new GamePerformanceEventArgs
                    {
                        FramesPerSecond = framesPerSecond,
                        ActiveActorCount = m_currentScene.GetActors().Count,
                        GameCurrentElapsedTime = (int)(DeltaTimeInSeconds * 1000),
                        EventListenerCount = EventListenerCount
                    });
                }

                m_framesThisReport = 0;
            }


            ElapsedTime = now - m_lastUpdateTime;
            m_lastUpdateTime = now;


            double elapsedSeconds = ElapsedTime.TotalSeconds;

            // cap it at 0.03's of a second so things don't go flying or pass through solids.
            if (elapsedSeconds > 0.03)
            {
                elapsedSeconds = 0.03;
            }

            // make sure it's never 0 or less than 0, make it a sixtieth of a second since we shouldn't be able
            // to go faster than that
            if (elapsedSeconds <= 0)
            {
                elapsedSeconds = 1.0 / 60.0;
            }

            DeltaTimeInSeconds = elapsedSeconds;

            if (m_pauseAfterFrame && m_pauseEventsCleared)
            {
            }
            else
            {
                Update(elapsedSeconds);
            }
        }



        private void Update(double deltaTimeInSeconds)
        {
            CurrentScene.Update(deltaTimeInSeconds);
            DoSceneChange(); // if we have a pending scene change, do it now.

            m_keysDownThisFrame.Clear();
            m_keysUpThisFrame.Clear();
            CurrentScene.ClearAllMouseArgsForNextUpdate();
            if (m_pauseAfterFrame)
            {
                DoPause();
            }
        }

        public override void SetValue(String propertyName, Object value)
        {
            base.SetValue(propertyName, value);


            if (propertyName == "Score")
            {
                Score = Utils.DoubleFromObject(value);
            }
        }

        public override Object GetValue(String propertyName)
        {
            switch (propertyName)
            {
                case "Score":
                    return Score;
                default:
                    return base.GetValue(propertyName);
            }
        }

        public MediaElement AddMediaElement(string url)
        {
            return AddMediaElement(url, false);
        }

        private Dictionary<string, MediaElement> m_sounds = new Dictionary<string, MediaElement>();
        public MediaElement AddMediaElement(string url, bool repeat)
        {
            if (m_sounds.ContainsKey(url))
            {
                return m_sounds[url];
            }
            else
            {
                MediaElement retVal = new MediaElement();

                retVal.Source = new Uri(url, UriKind.RelativeOrAbsolute);
                Console.Write("MSGG001: Adding media from " + retVal.Source);

                if (repeat)
                {
                    retVal.MediaEnded += OnMediaEndedReplay;
                }

                retVal.IsMuted = IsMuted;
                retVal.AutoPlay = false;

                PlayArea.Children.Add(retVal);

                m_sounds.Add(url, retVal);
                return retVal;
            }
        }

        private void OnMediaEndedReplay(Object sender, RoutedEventArgs args)
        {
            ((MediaElement)sender).Stop();
            ((MediaElement)sender).Play();
        }

        public void PlayAudio(string url)
        {
            PlayAudio(url, false);
        }

        public void PlayAudio(string url, bool repeat)
        {


            MediaElement me = AddMediaElement(url, repeat);
            me.AutoPlay = true;
            me.Stop();
            me.Play();
        }

        public void StopAudio(string url)
        {
            MediaElement me = AddMediaElement(url, false);
            if (me != null)
                me.Stop();
        }

        private void DoSceneChange() // (if any)
        {
            if (m_pendingNewScene == null)
            {
                return;
            }
            else
            {
                Scene previousScene = CurrentScene;

                CurrentScene = new Scene(this, m_gameInfo.Actors, m_gameInfo.Scenes[m_pendingNewScene]);

                // if moving from intro scene - start timing game duration
                if (previousScene.Type == Scene.SceneType.INTRO)
                {
                    m_gamePlayStart = DateTime.Now.Ticks;
                }

                // if going to win/loss screen - report score
                if (CurrentScene.Type == Scene.SceneType.WON || CurrentScene.Type == Scene.SceneType.LOST)
                {
                    // total game duration (note this includes paused time etc) - persist as member variable for signins
                    m_completedGameDuration = DateTime.Now.Ticks - m_gamePlayStart;

                    //ReportScore(false); - if you want to make a facebook app, this would be a good place to report the user's score
                }
            }
        }

        internal Data.SceneInfo FirstSceneInfo { get { return m_gameInfo.Scenes.getIndexedValue(0); } }
        public void ChangeScene(String sceneName)
        {
            if (m_gameInfo.Scenes.ContainsKey(sceneName))
            {
                Console.Write("MSGG003: Changing to scene \"" + sceneName + "\"");
                m_pendingNewScene = sceneName;
            }
            else
            {
                Console.ReportError(String.Format(@"Unable to change to scene ""{0}"" because no scene exists with that name.", sceneName));
            }
        }

        private void OnLostFocus(Object sender, EventArgs args)
        {
            if (!m_pauseAfterFrame && m_gameIsRunning)
            {
                Pause();
                Console.Write("MSGE001: LostFocus event raised");
            }
        }
        private bool m_pauseAfterFrame;
        public bool Pause()
        {
            // don't pause intro/won/lost/instructions scenes or if we've already paused
            if (CurrentScene.Type == Scene.SceneType.NORMAL && m_gameIsRunning)
            {
                m_pauseShowPauseScreen = true;
                m_pauseAfterFrame = true;
                m_pauseEventsCleared = false;
                return true;
            }
            else
            {
                return false;
            }
        }

        private Boolean _isMuted;
        public Boolean IsMuted
        {
            set
            {
                foreach (var keypair in m_sounds)
                {
                    keypair.Value.IsMuted = value;
                }
                _isMuted = value;
            }
            get
            {
                return _isMuted;
            }
        }
        private List<MediaElement> m_pausedSounds = new List<MediaElement>();
        private bool m_pauseEventsCleared;
        private bool m_pauseShowPauseScreen;
        private Canvas m_lastPauseScreen;
        private void DoPause()
        {
            if (m_pauseShowPauseScreen)
            {
                m_lastPauseScreen = new PauseScreen(this, CurrentScene.ViewportWidth, CurrentScene.ViewportHeight);

                m_overlayCanvas.Children.Add(m_lastPauseScreen);
                m_overlayCanvas.Visibility = Visibility.Visible;
            }
            m_gameIsRunning = false;
            m_pausedSounds.Clear();
            foreach (var soundKVP in m_sounds)
            {
                MediaElement sound = soundKVP.Value;
                if (sound.CurrentState == MediaElementState.Playing)
                {
                    if (sound.CanPause)
                    {
                        sound.Pause();
                        m_pausedSounds.Add(sound);
                    }
                }
            }
            List<KeyEventArgs> callUpOn = new List<KeyEventArgs>();
            foreach (var keydownKVP in m_keysDown)
            {
                callUpOn.Add(keydownKVP.Value);
            }

            foreach (KeyEventArgs arg in callUpOn)
            {
                OnKeyUp(null, arg);
            }


            CurrentScene.DoPause();
            m_pauseEventsCleared = true;

            if (PausedOrUnPaused != null)
                PausedOrUnPaused.Invoke(this, new PausedOrUnPausedArgs() { isPaused = true });
        }
        internal void UnPause()
        {
            m_gameIsRunning = true;
            if (m_lastPauseScreen != null)
                m_overlayCanvas.Children.Remove(m_lastPauseScreen);
            m_lastPauseScreen = null;
            m_overlayCanvas.Visibility = Visibility.Collapsed;

            foreach (MediaElement sound in m_pausedSounds)
            {
                sound.Play();
            }
            m_pauseAfterFrame = false;
            m_pauseEventsCleared = false;
            if (PausedOrUnPaused != null)
                PausedOrUnPaused.Invoke(this, new PausedOrUnPausedArgs() { isPaused = false });
        }

        private void OnKeyUp(Object sender, KeyEventArgs args)
        {
            m_keysDown.Remove(args.Key);

            if (m_keysUp.ContainsKey(args.Key) == false)
            {
                m_keysUpThisFrame[args.Key] = args;
                Console.Write("MSGE002: KeyUp (" + args.Key + ") event raised");
            }
            m_keysUp[args.Key] = args;

        }

        private void OnKeyDown(Object sender, KeyEventArgs args)
        {
            m_keysUp.Remove(args.Key);

            if (m_keysDown.ContainsKey(args.Key) == false)
            {
                m_keysDownThisFrame[args.Key] = args;
                Console.Write("MSGE003: KeyDown (" + args.Key + ") event raised");
            }
            m_keysDown[args.Key] = args;
        }


        /// <returns>The KeyEventArgs from when the key was pressed if the key is down.  Otherwise, returns null.</returns>
        public KeyEventArgs IsKeyDown(Key key)
        {
            KeyEventArgs retVal;
            m_keysDown.TryGetValue(key, out retVal);
            return retVal;
        }

        public KeyEventArgs IsKeyUp(Key key)
        {
            KeyEventArgs retVal;
            m_keysUp.TryGetValue(key, out retVal);
            return retVal;
        }

        public KeyEventArgs IsKeyDownThisFrame(Key key)
        {
            KeyEventArgs retVal;
            m_keysDownThisFrame.TryGetValue(key, out retVal);
            return retVal;
        }

        public KeyEventArgs IsKeyUpThisFrame(Key key)
        {
            KeyEventArgs retVal;
            m_keysUpThisFrame.TryGetValue(key, out retVal);
            return retVal;
        }

        private List<Actor> m_actorsListeningForUp = new List<Actor>();
        internal void RegisterForMouseUp(Actor actor)
        {
            m_actorsListeningForUp.Add(actor);
        }
        // Notify actors that had a mouse down of the mouse up, even if it occurs when not over the actor.
        private void OnMouseUp(Object sender, MouseButtonEventArgs args)
        {
            foreach (Actor actorToUp in m_actorsListeningForUp)
            {
                actorToUp.mouseCanvas_MouseLeftButtonUp(sender, args);
            }
            m_actorsListeningForUp.Clear();
        }

        private void OnMouseMove(Object sender, MouseEventArgs args)
        {
            MousePosition = args.GetPosition(CurrentScene.GetCanvasRoot());
        }
        public Point MousePosition { get; private set; }

        internal void ShowHowToPlay()
        {
            var howToSceneInfo =
                            (m_gameInfo.Scenes.First(
                                (scenePair) => (((SceneInfo)scenePair.Value).Type == "instructions")
                            ).Value) as SceneInfo;


            if (howToSceneInfo != null)
            {
                var howToScene = new Scene(this, m_gameInfo.Actors, howToSceneInfo);
                PlayArea.Children.Add(howToScene.GetCanvasRoot());
                FitSceneToCanvas(howToScene);

                m_pauseAfterFrame = true;
                m_pauseEventsCleared = false;
                m_pauseShowPauseScreen = false;

                howToScene.GetCanvasRoot().MouseLeftButtonDown += (sender, e) =>
                {
                    UnPause();
                };
                EventHandler<PausedOrUnPausedArgs> onUnpause = null;

                onUnpause = (sender, e) =>
                {
                    if (!((PausedOrUnPausedArgs)e).isPaused)
                    {
                        PlayArea.Children.Remove(howToScene.GetCanvasRoot());
                        FitSceneToCanvas(CurrentScene);
                        PausedOrUnPaused -= onUnpause;
                        IsHowToPlayShowing = false;
                    }
                };

                PausedOrUnPaused += onUnpause;

                if (m_lastPauseScreen != null)
                    m_overlayCanvas.Children.Remove(m_lastPauseScreen);
                IsHowToPlayShowing = true;
            }
        }
#if !PHONE
        #region CustomCodeSupport
        Dictionary<string, object> customDict = new Dictionary<string, object>();

        public Actor SpawnActor(JSObject obj)
        {
            double x = 0;
            double y = 0;
            Microsoft.Popfly.GameCreator.Data.EffectInfo effectInfo = new Microsoft.Popfly.GameCreator.Data.EffectInfo() { Fade = false, Scale = false };

            if (!(obj["x"] is UnDefined))
            {
                x = Convert.ToDouble(obj["x"]);
            }
            if (!(obj["y"] is UnDefined))
            {
                y = Convert.ToDouble(obj["y"]);
            }
            if (!(obj["effect"] is UnDefined))
            {
                JSObject effect = (JSObject)obj["effect"];
                effectInfo.Fade = !(effect["fade"] is UnDefined) && (Boolean)effect["fade"];
                effectInfo.Scale = !(effect["scale"] is UnDefined) && (Boolean)effect["scale"];
            }

            if (obj["parent"] is UnDefined)
            {
                return this.CurrentScene.AddActor(x, y, obj["actor"].ToString(), effectInfo);
            }
            else
            {
                return this.CurrentScene.AddActor((Actor)obj["parent"], obj["actor"].ToString());
            }
        }
        //Called by the CreateMotionBehavior in the javascript code
        public void _CreateMotionBehavior(BehaviorOwner context, JSObject param)
        {
            //TODO: add towards actor support
            var info = new MotionInfo()
            {
                Acceleration = param.GetAsType<double>("acceleration", 0.0),
                AutoReverse = param.GetAsType<bool>("autoReverse", false),
                ContinueMoving = param.GetAsType<MotionInfo.ContinueMovingValues>("continueMoving", MotionInfo.ContinueMovingValues.NotDefined),
                Direction = param.GetAsType<MotionInfo.DirectionValues>("direction", MotionInfo.DirectionValues.NotDefined),
                Distance = param.GetAsType<double>("distance", 0.0),
                Duration = param.GetAsType<double>("duration", 0.0),
                IgnoreMouseX = param.GetAsType<bool>("ignoreMouseX", false),
                IgnoreMouseY = param.GetAsType<bool>("ignoreMouseY", false),
                MaxSpeed = param.GetAsType<double>("maxSpeed", 0.0),
                Modifies = param.GetAsType<MotionInfo.ModifiesValues>("modifies", MotionInfo.ModifiesValues.NotDefined),
                Relative = param.GetAsType<MotionInfo.RelativeValues>("relative", MotionInfo.RelativeValues.NotDefined),
                Repeat = param.GetAsType<String>("repeat", ""),
                Scene = param.GetAsType<String>("scene", null),
                SubType = param.GetAsType<String>("subType", null),
                TargetPointX = param.GetAsType<double>("targetPointX", 0.0),
                TargetPointY = param.GetAsType<double>("targetPointX", 0.0),
                Transform = param.GetAsType<String>("transform", null),
                Type = param.GetAsType<String>("type", ""),
                VelocityJump = param.GetAsType<double>("velocityJump", 0.0),
                Includes = param.GetAsType<Collections.OrderedDictionary<String, Data.IncludeObjectInfo>>("includes", null,
                    obj => (from key in ((JSObject)obj).Keys where (bool)((JSObject)obj)[key] select key)
                                .ToDictionary(
                                    (val => val),
                                    (val => (new Data.IncludeObjectInfo() { Value = val.ToString() }))
                                )

                ),
            };
            MotionBehaviorFactory.CreateMotionBehavior(this, context, info).Invoke(context, null);
        }
        #endregion
#endif

        #region IDisposable Members

        public void Dispose()
        {
            m_eventControl.LostFocus -= OnLostFocus;
            PlayArea.MouseMove -= OnMouseMove;
            PlayArea.MouseLeftButtonUp -= OnMouseUp;
            m_eventControl.KeyUp -= OnKeyUp;
            m_eventControl.KeyDown -= OnKeyDown;
            if (TestMode)
            {
                CompositionTarget.Rendering -= TestLoopRenderingHandler;
            }
            else
            {
                CompositionTarget.Rendering -= GameLoopRenderingHandler;
            }
        }

        #endregion

    }
}
